﻿
Imports System.Threading
Imports std

#Const threadSolution = True
'this class is designed to be work in STA, and avoid multiple threads to cost more system resource
Public Class timer2
    Public Delegate Sub timer2Tick()
    Public Delegate Sub timer2Tick3(ByRef obj As Object)
#If Not threadSolution Then
    Public Delegate Sub timer2Tick2(ByVal e As Timers.ElapsedEventArgs)
    Public Delegate Sub timer2Tick4(ByVal e As Timers.ElapsedEventArgs, ByRef obj As Object)
#Else
    Public Delegate Sub timer2Tick2(ByVal tick As Int64)
    Public Delegate Sub timer2Tick4(ByVal tick As Int64, ByRef obj As Object)
#End If
#If Not threadSolution Then
    Private WithEvents timer As Timers.Timer = Nothing
#Else
    Private thread As Thread = Nothing
    Private intervalMS As Int64 = 1000
#End If

    Public defaultSleepTimeMS As Int64 = 1

    Private Class callType
        Public tick As timer2Tick = Nothing
        Public tick3 As timer2Tick3 = Nothing
        Public tick2 As timer2Tick2 = Nothing
        Public tick4 As timer2Tick4 = Nothing
        Public obj As Object = Nothing
        Public intervalMS As UInt64 = 0
        Public lastcall As Int64 = 0
        Public guid As Guid = System.Guid.NewGuid()
    End Class

    Private removequeue As setqueue(Of Guid) = Nothing
    Private calls As vector(Of callType) = Nothing

    Public Sub [stop]()
#If threadSolution Then
#If PocketPC OrElse Smartphone Then
        If Not thread Is Nothing Then
            Try
                thread.Abort()
                thread.Join()
            Catch
            End Try
        End If
#Else
        If Not thread Is Nothing AndAlso thread.IsAlive() Then
            thread.Abort()
            thread.Join()
        End If
#End If
#Else
        timer.stop()
#End If
    End Sub

    Private Function addcall(ByVal [call] As callType) As Guid
        [call].lastcall = ticksToMilliseconds(Now.Ticks()) - [call].intervalMS

        SyncLock (calls)
            calls.push_back([call])
        End SyncLock

#If Not threadSolution Then
        SyncLock (timer)
            timer.intervalMS = gcd(timer.intervalMS, [call].intervalMS)
            If timer.Enabled = False Then
                timer.Start()
            End If
        End SyncLock
#Else
        SyncLock thread
            intervalMS = gcd(intervalMS, [call].intervalMS)
#If Not (PocketPC OrElse Smartphone) Then
            If thread.ThreadState = ThreadState.Unstarted Then
                thread.Start()
            End If
#Else
            thread.Start()
#End If
        End SyncLock
#End If

        Return [call].guid
    End Function

    Private Function addcall(ByVal tickfunc As [Delegate], ByVal intervalMS As UInt64 _
                             , ByRef obj As Object, ByVal tickIndex As Int32) As Guid
        If tickfunc Is Nothing Then
            raiseError("cannot addcall to a non-existing invoke delegate.", errorHandle.errorType.warning)
            Return Guid.Empty
        End If

        If isdebugmode() Then
            raiseError("timer2 will addcall to method " + getInvokeMethodName(tickfunc) _
                       + " with intervalMS " + Convert.ToString(intervalMS))
        End If

        Dim c As callType = Nothing
        c = New callType()
        Select Case tickIndex
            Case 1
                c.tick = tickfunc
            Case 2
                c.tick2 = tickfunc
            Case 3
                c.tick3 = tickfunc
            Case 4
                c.tick4 = tickfunc
            Case Else
                assert(False, "should not have other tick index.")
        End Select
        c.intervalMS = intervalMS
        c.obj = obj

        Return addcall(c)
    End Function

    Public Function addcall(ByVal tickfunc As timer2Tick, ByVal intervalMS As UInt64) As Guid
        Return addcall(tickfunc, intervalMS, Nothing, 1)
    End Function

    Public Function addcall(ByVal tickfunc As timer2Tick2, ByVal intervalMS As UInt64) As Guid
        Return addcall(tickfunc, intervalMS, Nothing, 2)
    End Function

    Public Function addcall(ByVal tickfunc As timer2Tick3, ByVal intervalMS As UInt64, ByRef obj As Object) As Guid
        Return addcall(tickfunc, intervalMS, obj, 3)
    End Function

    Public Function addcall(ByVal tickfunc As timer2Tick4, ByVal intervalMS As UInt64, ByRef obj As Object) As Guid
        Return addcall(tickfunc, intervalMS, obj, 4)
    End Function

#If RETIRED Then
    Public Function removecall(ByVal index As Int64) As Boolean
        assert(mutex.WaitOne(), "cannot lock mutex when removecall")
        Dim rtn As Boolean = False
        If calls Is Nothing Then
            rtn = False
        Else
            rtn = calls.erase(index)
        End If
        mutex.ReleaseMutex()

        Return rtn
    End Function
#End If

    Public Function availableGuid(ByVal guid As Guid) As Boolean
        Return getGuidIndex(guid) <> npos
    End Function

    Public Function getGuidIndex(ByVal guid As Guid) As Int64
        SyncLock calls
            Return getGuidIndexImpl(guid)
        End SyncLock
    End Function

    Private Function getGuidIndexImpl(ByVal guid As Guid) As Int64
        Dim i As Int64
        For i = 0 To calls.size() - 1
            If calls(i).guid = guid Then
                Exit For
            End If
        Next
        Return ifoper(i < calls.size(), i, npos)
    End Function

    'avoid deadlock here
    Public Function removecall(ByVal guid As Guid) As Boolean
        Dim rtn As Boolean = False
        rtn = availableGuid(guid)
        If rtn Then
            SyncLock removequeue
                removequeue.push(guid)
            End SyncLock
        End If

        Return rtn
    End Function

#If threadSolution Then
    Private Sub workthread()
        Dim ticks As Int64
        While True
            ticks = Now.Ticks()
            eachround()
            ticks = millisecondsToTicks(intervalMS) - (Now.Ticks() - ticks)
            If ticks > 0 Then
                sleep(ifoper(ticks > maxInt64, defaultSleepTimeMS, ticksToMilliseconds(ticks)))
            End If
        End While
    End Sub
#End If

#If Not threadSolution Then
    Private Sub startcall(ByVal c As callType, ByVal callms As Int64, ByVal e As Timers.ElapsedEventArgs)
#Else
    Private Sub startcall(ByVal c As callType, ByVal callms As Int64)
#End If
        If callms - c.lastcall >= c.intervalMS Then
#If RETIRED Then
            If isdebugmode() AndAlso callms - c.lastcall >= (c.intervalMS << 2) Then
                raiseError("caught some calls have not been called for over 4*intervalMS," _
                           + " some call functions cost too much time.", errorHandle.errorType.warning)
            End If
#End If
            Try
                If Not c.tick Is Nothing Then
                    c.tick()
                ElseIf Not c.tick3 Is Nothing Then
                    c.tick3(c.obj)
#If Not threadSolution Then
            ElseIf Not c.tick2 Is Nothing Then
                c.tick2(e)
            ElseIf Not c.tick4 Is Nothing Then
                c.tick4(e, c.obj)
#Else
                ElseIf Not c.tick2 Is Nothing Then
                    c.tick2(callms)
                ElseIf Not c.tick4 Is Nothing Then
                    c.tick4(callms, c.obj)
#End If
                Else
                    assert(False, "should not have a call with no invoke available.")
                End If
            Catch ex As ThreadAbortException
                raiseError("thread abort.", errorHandle.errorType.warning)
            Catch ex As Exception
                raiseError("caught unexpected exception, ex " + ex.Message + ", call stack " + ex.StackTrace(), _
                           errorHandle.errorType.critical)
            End Try
            c.lastcall = callms
        End If
    End Sub

#If Not threadSolution Then
    Private Sub startcall(ByVal c As callType, ByVal e As Timers.ElapsedEventArgs)
        startcall(c, ticksToMilliseconds(e.SignalTime().Ticks()), e)
    End Sub
#Else
    Private Sub startcall(ByVal c As callType)
        startcall(c, ticksToMilliseconds(Now.Ticks()))
    End Sub
#End If

#If Not threadSolution Then
    Private Sub timer_Elapsed(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs) Handles timer.Elapsed
        'keep one thread at any time with AutoReset property
#Else
    Private Sub eachround()
#End If
        assert(Not calls Is Nothing, "should not have a timer.Elapsed event before added any call.")
        Dim i As Int64
        SyncLock removequeue
            While Not removequeue.empty()
                Dim guid As Guid = Nothing
                guid = removequeue.front()
                removequeue.pop()
                i = getGuidIndex(guid)
                'this can be in as guid is a random number, cannot determine whether there are two with same value.
                'to avoid break logic, should assert and restart now.
                'some calls may be added during last timer.Elapsed, so ?
                assert(i <> npos, "caught an invalid guid to remove, i = npos, guid = " + Convert.ToString(guid))
                'assume except for here, calls can only increase but not decrease elements
                ', so the j returns from getGuidIndex is always available.
                SyncLock calls
                    assert(calls.erase(i), "caught an invalid guid to remove, calls.erase(j) returns false.")
                End SyncLock
            End While
        End SyncLock

        'enable to add a call when in timer.Elapsed event
        i = 0
        While i < calls.size()
#If Not threadSolution Then
            startcall(calls(i), e)
#Else
            startcall(calls(i))
#End If
            i += 1
        End While

#If Not threadSolution Then
        'start again for next round.
        timer.Start()
#End If
    End Sub

    Public Sub New()
        calls = New vector(Of callType)()
#If Not threadSolution Then
        timer = New Timers.Timer()
        timer.Enabled = False
        timer.AutoReset = False
#Else
        thread = New Thread(AddressOf workthread)
        thread.Name() = "TIMER2_WORKTHREAD"
#End If
        removequeue = New setqueue(Of Guid)
    End Sub

    Protected Overrides Sub Finalize()
#If threadSolution Then
        thread.Abort()
#Else
        timer.Close()
        timer.Dispose()
#End If
        MyBase.Finalize()
    End Sub
End Class
